Skip to content

feat(metrics): Dashboard charts performance improvements#3083

Merged
ericallam merged 4 commits intomainfrom
metrics-chart-max-series
Feb 18, 2026
Merged

feat(metrics): Dashboard charts performance improvements#3083
ericallam merged 4 commits intomainfrom
metrics-chart-max-series

Conversation

@matt-aitken
Copy link
Member

Summary

  • Only render the top 50 series
  • Improved rendering performance on bar charts

@changeset-bot
Copy link

changeset-bot bot commented Feb 18, 2026

⚠️ No Changeset found

Latest commit: df882e5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@matt-aitken matt-aitken marked this pull request as ready for review February 18, 2026 15:58
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

Walkthrough

This pull request introduces a series-limiting feature to chart rendering, capping the number of rendered series at 50 (MAX_SERIES) to manage SVG rendering budget. The changes compute the total series count from the data and filter to show only the top series by absolute total values. A Callout component is displayed when truncation occurs. Simultaneously, the activePayload state management is refactored from the HighlightState hook into a separate PayloadContext to optimize re-renders. All chart components (Bar, Line, Legend) are updated to consume visibleSeries instead of all dataKeys for rendering, and to use the new setActivePayload function. The ChartRoot component gains visibleSeries and beforeLegend props that are threaded through the provider and inner components. Dynamic effective max points are calculated based on the filtered series count to maintain SVG budget constraints.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Key changes across files

  • QueryResultsChart.tsx: Introduced series limiting logic with totalSeriesCount tracking, dynamic effectiveMaxPoints calculation, and truncation warning UI.
  • ChartContext.tsx: Extended context with visibleSeries and new setActivePayload mechanism; created separate PayloadContext to isolate activePayload re-renders.
  • ChartBar.tsx & ChartLine.tsx: Updated to render based on visibleSeries instead of full dataKeys, and to call setActivePayload instead of highlight.setActivePayload.
  • ChartLegendCompound.tsx: Switched from highlight.activePayload to new useActivePayload hook for hover state calculations.
  • ChartRoot.tsx: Added visibleSeries and beforeLegend props with prop threading through provider and inner components.
  • useHighlightState.ts: Removed activePayload and setActivePayload, simplified to focus on bar highlighting state.
  • useZoomSelection.ts: Added memoization to return value for optimization.
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provides a brief summary of key changes but does not follow the required template structure. Required sections (Checklist, Testing, Changelog, Screenshots) are missing or incomplete. Add the missing sections from the template: complete the checklist, describe testing steps, expand the Changelog section, and add any relevant screenshots or clarification about the 30 vs 50 series discrepancy.
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective: improving dashboard charts performance by implementing series rendering limits and hover optimization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch metrics-chart-max-series

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines 130 to +133
...totals,
...hoverData,
};
}, [highlight.activePayload, totals]);
}, [activePayload, totals]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Legend shows stale hover data for non-visible (truncated) series when hovering chart

When visibleSeries is a strict subset of dataKeys (i.e., some series are rendered in the legend but not on the chart), hovering a data point on the chart only produces activePayload entries for the visible/rendered series. The legend's currentData computation at ChartLegendCompound.tsx:113-133 merges hover data with totals, so non-visible series fall back to their aggregate totals while visible series show the hovered point's value.

This means the legend mixes point-specific values (for visible series) with aggregate totals (for non-visible series) during hover. This is arguably the best available behavior given that non-visible series have no SVG element to provide point-level data, but it could be confusing if users notice the numbers don't sum correctly. Worth considering whether non-visible series should show a dash (–) during hover instead.

(Refers to lines 112-133)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/webapp/app/components/primitives/charts/ChartLine.tsx (1)

127-131: Minor: setTooltipActive(false) is redundant before reset().

In handleMouseLeave, highlight.reset() already sets tooltipActive: false (via initialState). The preceding setTooltipActive(false) is harmless but unnecessary.

Also applies to: 144-151

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/components/primitives/charts/ChartLine.tsx` around lines 127
- 131, Remove the redundant explicit tooltip toggle before calling reset: in the
handleMouseLeave handler, drop the highlight.setTooltipActive(false) call and
just call highlight.reset(); do the same for the other similar handler block
around the 144-151 region (remove the setTooltipActive(false) before
highlight.reset()). This keeps the code concise because highlight.reset()
already restores tooltipActive via the component's initial state.
apps/webapp/app/components/primitives/charts/ChartContext.tsx (1)

52-52: Consider a narrower type than any[] for payload.

PayloadContext and useActivePayload both use any[] | null. Since this is the recharts tooltip payload, you could use recharts' Payload type for slightly better type safety. Not critical given the existing any usage across chart components, but worth considering if you tighten types later.

Also applies to: 63-64

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/components/primitives/charts/ChartContext.tsx` at line 52,
Replace the broad any[]|null with Recharts' Payload type: update PayloadContext
(createContext<any[] | null>(null)) and the useActivePayload return/type
annotations to use Payload[] | null, and add an import for the Payload type from
Recharts (e.g., import { Payload } from
'recharts/types/component/DefaultTooltipContent' or the appropriate recharts
export) so the tooltip payload has stronger typing.
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59b6eb9 and df882e5.

📒 Files selected for processing (8)
  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/primitives/charts/ChartBar.tsx
  • apps/webapp/app/components/primitives/charts/ChartContext.tsx
  • apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx
  • apps/webapp/app/components/primitives/charts/ChartLine.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
  • apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts
  • apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: Always import tasks from @trigger.dev/sdk, never use @trigger.dev/sdk/v3 or deprecated client.defineJob pattern
Every Trigger.dev task must be exported and have a unique id property with no timeouts in the run function

Files:

  • apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts
  • apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx
  • apps/webapp/app/components/primitives/charts/ChartBar.tsx
  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/primitives/charts/ChartContext.tsx
  • apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts
  • apps/webapp/app/components/primitives/charts/ChartLine.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts
  • apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx
  • apps/webapp/app/components/primitives/charts/ChartBar.tsx
  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/primitives/charts/ChartContext.tsx
  • apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts
  • apps/webapp/app/components/primitives/charts/ChartLine.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Import from @trigger.dev/core using subpaths only, never import from root

Files:

  • apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts
  • apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx
  • apps/webapp/app/components/primitives/charts/ChartBar.tsx
  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/primitives/charts/ChartContext.tsx
  • apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts
  • apps/webapp/app/components/primitives/charts/ChartLine.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts
  • apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx
  • apps/webapp/app/components/primitives/charts/ChartBar.tsx
  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/primitives/charts/ChartContext.tsx
  • apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts
  • apps/webapp/app/components/primitives/charts/ChartLine.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Access environment variables via env export from apps/webapp/app/env.server.ts, never use process.env directly

Files:

  • apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts
  • apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx
  • apps/webapp/app/components/primitives/charts/ChartBar.tsx
  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/primitives/charts/ChartContext.tsx
  • apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts
  • apps/webapp/app/components/primitives/charts/ChartLine.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts
  • apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts
  • apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx
  • apps/webapp/app/components/primitives/charts/ChartBar.tsx
  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/primitives/charts/ChartContext.tsx
  • apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts
  • apps/webapp/app/components/primitives/charts/ChartLine.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
🧠 Learnings (3)
📚 Learning: 2026-02-11T16:37:32.429Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/components/primitives/charts/Card.tsx:26-30
Timestamp: 2026-02-11T16:37:32.429Z
Learning: In projects using react-grid-layout, avoid relying on drag-handle class to imply draggability. Ensure drag-handle elements only affect dragging when the parent grid item is configured draggable in the layout; conditionally apply cursor styles based on the draggable prop. This improves correctness and accessibility.

Applied to files:

  • apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx
  • apps/webapp/app/components/primitives/charts/ChartBar.tsx
  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/primitives/charts/ChartContext.tsx
  • apps/webapp/app/components/primitives/charts/ChartLine.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Use `useRun`, `useRealtimeRun` and other SWR/realtime hooks from `trigger.dev/react-hooks` for data fetching

Applied to files:

  • apps/webapp/app/components/primitives/charts/ChartContext.tsx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Use `trigger.dev/react-hooks` package for realtime subscriptions in React components

Applied to files:

  • apps/webapp/app/components/primitives/charts/ChartContext.tsx
🧬 Code graph analysis (3)
apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx (1)
apps/webapp/app/components/primitives/charts/ChartContext.tsx (1)
  • useActivePayload (63-65)
apps/webapp/app/components/code/QueryResultsChart.tsx (3)
apps/webapp/app/components/primitives/Callout.tsx (1)
  • Callout (73-170)
apps/webapp/app/components/primitives/charts/ChartBlankState.tsx (1)
  • ChartBlankState (4-23)
apps/webapp/app/components/primitives/charts/ChartCompound.tsx (1)
  • Chart (92-98)
apps/webapp/app/components/primitives/charts/ChartContext.tsx (2)
apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts (1)
  • UseHighlightStateReturn (23-23)
apps/webapp/app/components/primitives/charts/Chart.tsx (1)
  • ChartConfig (11-20)
🔇 Additional comments (12)
apps/webapp/app/components/code/QueryResultsChart.tsx (2)

609-665: Good approach: computing group totals in a single pass before building heavy data objects.

Accumulating groupTotals alongside the row iteration and then selecting only the top-N groups before constructing the per-point data objects avoids allocating thousands of keys per data point. Clean optimization.


562-566: Dynamic SVG budget calculation looks correct.

The effectiveMaxPoints calculation is sound: MAX_SVG_ELEMENT_BUDGET / seriesCount, clamped between MIN_DATA_POINTS and MAX_DATA_POINTS. Both the grouped and non-grouped paths use consistent logic.

Also applies to: 667-671

apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts (1)

38-77: Clean refactor: memoized return and functional updaters to reduce re-renders.

The early-return checks in setHoveredLegendItem and setTooltipActive prevent unnecessary state transitions, and the useMemo wrapper gives consumers a stable object reference. Well done.

apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts (1)

178-190: Consistent memoization pattern with useHighlightState.

Stable callbacks (via useCallback + stateRef) mean the memo only recomputes on state changes. Good alignment with the broader re-render reduction effort.

apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx (1)

56-57: Good separation: legend consumes payload from PayloadContext independently.

This isolates legend re-renders from chart element re-renders. The activePayload from useActivePayload() correctly replaces all former highlight.activePayload references, while highlight.activeBarKey is still used for series-level highlighting (lines 153, 203). Clean split.

apps/webapp/app/components/primitives/charts/ChartLine.tsx (1)

81-81: Rendering switched to visibleSeries throughout — looks correct.

The stacked guard, Area map, and Line map all consistently use visibleSeries from context, ensuring only the capped subset of series generates SVG elements.

Also applies to: 133-133, 164-164, 212-212

apps/webapp/app/components/primitives/charts/ChartRoot.tsx (1)

69-120: Clean prop threading of visibleSeries and beforeLegend.

Both new props are correctly forwarded: visibleSeriesChartProvider (context), beforeLegendChartRootInner (render). The existing hooks (useHasNoData, useSeriesTotal) correctly continue to use dataKeys (all series) rather than visibleSeries.

apps/webapp/app/components/primitives/charts/ChartContext.tsx (2)

48-65: Effective context split for payload isolation.

Separating activePayload into its own PayloadContext is a well-targeted optimization — frequent mouse-move updates only re-render the legend (via useActivePayload), not the chart SVG elements consuming ChartCompoundContext. The dedup via activeTooltipIndexRef (lines 107–117) further reduces state churn on same-index mouse moves.


119-130: The code correctly maintains referential identity for non-reset properties.

The highlightWithReset implementation properly spreads highlight (which includes activeBarKey, activeDataPointIndex, tooltipActive, setHoveredBar, setHoveredLegendItem, and setTooltipActive) and overrides only the reset method. Since resetWithPayload depends on originalReset and the entire object is memoized with [highlight, resetWithPayload] as dependencies, the identity updates appropriately whenever either source changes—the correct and expected behavior.

apps/webapp/app/components/primitives/charts/ChartBar.tsx (3)

86-89: handleMouseLeave correctly chains zoom + highlight reset.

Since highlight.reset is now resetWithPayload (via ChartContext), this also clears activePayload and the tooltip index ref. Good integration.


68-68: Context consumption expanded cleanly.

visibleSeries and setActivePayload are destructured from context and used consistently: visibleSeries for the Bar map, setActivePayload for mouse-move events.


187-220: Behavior change: dimming now applies per-series, not per-cell.

The previous Cell-based rendering allowed highlighting a single bar at a specific data point (via getBarOpacity checking both activeBarKey and activeDataPointIndex). The new fillOpacity on the Bar component dims/brightens the entire series at once. This is a good trade-off for performance with many series, but note that hovering a specific bar now highlights the entire series strip rather than just that cell.

Also, getBarOpacity in useHighlightState.ts is now unused and can be removed.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/webapp/app/components/code/QueryResultsChart.tsx`:
- Around line 820-835: The callout logic currently compares totalSeriesCount to
series.length which is always false in the non-grouped path; update the
condition that builds seriesLimitCallout to compare totalSeriesCount against
visibleSeries.length (or sortedSeries.length) instead so the warning is shown
whenever visibleSeries is truncated; locate the relevant variables
visibleSeries, totalSeriesCount, series, sortedSeries and change the conditional
expression used to render seriesLimitCallout accordingly.

---

Nitpick comments:
In `@apps/webapp/app/components/primitives/charts/ChartContext.tsx`:
- Line 52: Replace the broad any[]|null with Recharts' Payload type: update
PayloadContext (createContext<any[] | null>(null)) and the useActivePayload
return/type annotations to use Payload[] | null, and add an import for the
Payload type from Recharts (e.g., import { Payload } from
'recharts/types/component/DefaultTooltipContent' or the appropriate recharts
export) so the tooltip payload has stronger typing.

In `@apps/webapp/app/components/primitives/charts/ChartLine.tsx`:
- Around line 127-131: Remove the redundant explicit tooltip toggle before
calling reset: in the handleMouseLeave handler, drop the
highlight.setTooltipActive(false) call and just call highlight.reset(); do the
same for the other similar handler block around the 144-151 region (remove the
setTooltipActive(false) before highlight.reset()). This keeps the code concise
because highlight.reset() already restores tooltipActive via the component's
initial state.

@ericallam ericallam changed the title Charts performance improvements feat(metrics): Dashboard charts performance improvements Feb 18, 2026
@ericallam ericallam merged commit eb0f963 into main Feb 18, 2026
42 checks passed
@ericallam ericallam deleted the metrics-chart-max-series branch February 18, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments